descriptor
就像是Python的倉庫管理員之一,在某些情況下(如[Day10]所述):
non-data descriptor
時,可以提供取的功能。data descriptor
時,同時提供存取功能。所以要從哪邊取值及要將值存去哪裡,是寫descriptor
時,最須注意的地方。
一般來說,有兩個地方可以考慮,一個是desc_instance
內,另一個是instance.__dict__
,兩者都各有一些眉角。
接下來兩天,我們將練習數個data descriptor
的寫法(註1
)。今天會先分享一些有潛在問題的寫法,透過了解各方法的缺點或限制,漸漸學習反思。明天再分享一些通用的寫法。
以下將會用desc_instance
來代稱data descriptor instance
。
方法1
試著將給定的值作為instance variable
存在desc_instance
內,即# 01
中的self._value
。
# 01
class Desc:
def __get__(self, instance, owner_cls):
return self._value
def __set__(self, instance, value):
self._value = value
class MyClass:
x = Desc()
if __name__ == '__main__':
my_inst1, my_inst2 = MyClass(), MyClass()
# my_inst1
my_inst1.x = 1
print(f'{my_inst1.x=}') # 1
# my_inst2
print(f'{my_inst2.x=}') # 1
my_inst2.x = 2
print(f'{my_inst2.x=}') # 2
# my_inst1.x also changed
print(f'{my_inst1.x=}') # 2...not 1
my_inst1.x=1
my_inst2.x=1
my_inst2.x=2
my_inst1.x=2
MyClass
中建立名為x
的desc_instance
。my_inst1
及my_inst2
兩個instance
。此時若使用my_inst1.x
或my_inst2.x
,將會呼叫x.__get__
取值:若使用my_inst1.x = 1
或my_inst2.x = 2
,將會呼叫x.__set__
賦值。my_inst1.x = 1
將1
指定給x
內的self._value
,並確認my_inst1.x
的確為1
。my_inst2.x
,發現其已經有1
這個值。這是因為x
現在是一個class variable
,所以my_inst2.x
就是my_inst1.x
。my_inst2.x = 2
會將2
指定給x
內的self._value
,也就是說my_inst1.x
及my_inst2.x
的回傳值都會是2
。my_inst1.x = 1
的語法,指定值給self._value
。MyClass
生成的instance
都擁有能修改x
的權力。方法2
試著將給定的值作為instance variable
存在instance
本身,即# 02
中的instance.hardcoded_name
。
# 02
class Desc:
def __get__(self, instance, owner_cls):
return getattr(instance, 'hardcoded_name', None)
def __set__(self, instance, value):
setattr(instance, 'hardcoded_name', value)
class MyClass:
x = Desc()
y = Desc()
if __name__ == '__main__':
my_inst = MyClass()
print(f'{my_inst.__dict__=}') # {}
my_inst.x = 1
print(f'{my_inst.x=}') # 1
print(f'{my_inst.__dict__=}') # {'hardcoded_name': 1}
my_inst.x = 2
print(f'{my_inst.x=}') # 2
print(f'{my_inst.y=}') # 2
print(f'{my_inst.__dict__=}') # {'hardcoded_name': 2}
my_inst.__dict__={}
my_inst.x=1
my_inst.__dict__={'hardcoded_name': 1}
my_inst.x=2
my_inst.y=2
my_inst.__dict__={'hardcoded_name': 2}
MyClass
中建立x
及y
兩個desc_instance
。my_inst.__dict__
為一空dict
。my_inst.x = 1
將1
指定給my_inst.hardcoded_name
,透過再次觀察my_inst.__dict__
,可以確認hardcoded_name
已在instance.__dict__
中,且其值為1
。my_inst
只有準備一個hardcoded_name
來作為存取descriptor
的倉庫。所以當我們使用my_inst.x = 2
時,其實相當於將2
指定給my_inst.hardcoded_name
,透過再次觀察MyClass.__dict__
,可以確認hardcoded_name
已在instance.__dict__
中,且其值已變為2
。MyClass
中,所有Desc
生成的desc_instance
將會共享一個固定的instance variable
,即instance.hardcoded_name
。方法3
嘗試於descriptor
內建立一個dict
,即# 03
的self._data
。我們以各instance
本身為self._data
的key
,於__set__
給定的value
為self._data
的value
。
# 03
class Desc:
def __init__(self):
self._data = {}
def __get__(self, instance, owner_cls):
return self._data.get(instance)
def __set__(self, instance, value):
self._data[instance] = value
class MyClass:
x = Desc()
y = Desc()
如果進行和方法1
及方法2
類似的檢查,可以發現方法3
也沒有類似的問題,但卻有一個非常大的缺點。
self._data
是將instance
本身作為key
,這代表即使我們手動使用del
指令刪除了instance
,也會是個假象,instance
不會被gc
(garbage collect
),因為至少還有一個strong reference
存在。這是個嚴重的memoey leak
,該被gc
的obj
卻還是存在,且有機會被存取。memoey leak
,我們還必須確定instance
是hashalbe
,才能作為self._data
的key
。方法4
類似於方法3
,但這次我們使用id(instance)
為self._data
的key
。
# 04
class Desc:
def __init__(self):
self._data = {}
def __get__(self, instance, owner_cls):
return self._data.get(id(instance))
def __set__(self, instance, value):
self._data[id(instance)] = value
class MyClass:
x = Desc()
y = Desc()
如果進行和方法1
及方法2
類似的檢查,可以發現方法4
也沒有類似的問題,且如果我們利用del
來刪除instance
時,該instance
也真的會被gc
。僅管如此,方法4
還是有一些缺點。
instance
,其記憶體位置id(instance)
,仍然以int
型態存在self._data
中。乍聽好像沒什麼關係,但是Python的記憶體位置是會重複使用的。如果這個記憶體位置被其它obj
使用了,我們的descriptor
就隱含了這個不相關obj
的資訊(雖然機率極低)。descriptor
,也會造成很多無謂的記憶體浪費。上面幾個方法讓我們對descriptor
有了基本的認知。現在我們來充充電,講幾個實作descriptor
時常會用到的觀念,為明天descriptor
的通用寫法做好準備。
desc_instance
當我們應用descriptor
時,有時需要取得desc_instance
,一個常見的做法如下:
def __get__(self, instance, owner_cls):
if instance is None:
return self
...
在__get__
一開始,先判斷instance
是不是None
。如果是None
,代表我們是由class
來取,直接返回desc_instance
。也就是說,當我們想取得desc_instance
時,可以利用MyClass.desc
來取得。雖然我們大多數情況都是使用instance
呼叫desc_instance
,但當想觀察desc_instance
內部狀態時,可以透過這個小技巧來達成。從明天的方法5
開始,我們會於__get__
中加上這一段程式碼。
__set_name__
__set_name__
的signature
如下:
__set_name__(self, owner_cls, name)
class
在定義時,會自動尋找有實作__set_name__
的attribute
,透過這個方法將他們在class
的名字傳入desc_instance
(註2
)。舉例來說,如果Desc
實作有__set_name__
,那麼MyClass
中的'x'
(str
型態)於MyClass
定義時,就會自動透過x.__set_name__
傳入desc_instance
。
class MyClass:
x = Desc()
這個功能非常方便,可以讓我們在設計descriptor
時,取得於MyClass
中定義的名字。
__slots__
當class
實作有__slots__
(一般定義為tuple
),未被列在其中的attribute
,將無法由instance
存取,這包括我們一直習以為常使用的__dict__
。當然您也可以選擇使用__slots__
然後手動將__dict__
加進__slots__
。
由# 101
可以看出,當MyClass
的__slots__
設定為空的tuple
時,我們無法使用my_inst.__dict__
。但是在MyClass2
中,我們手動加入__dict__
後,就可以存取my_inst2.__dict__
了。
所以當我們說一個instance.__dict__
可用時,代表其class
未使用slots
又或者有使用slots
但是有將__dict__
加進__slots__
。
# 101
class MyClass:
__slots__ = ()
class MyClass2:
__slots__ = ('__dict__',)
if __name__ == '__main__':
my_inst = MyClass()
print(f'{my_inst.__dict__=}') # AttributeError
my_inst2 = MyClass2()
print(f'{my_inst2.__dict__=}') # {}
或許您會想,應該不會有很多Class
使用slots
吧?但事實上,當程式需要大量由同個class
生成的instance
時,選擇使用slots
,可以節省滿多的記憶體消耗。在一些與database
相關的ORM
應用或iterator class
的實作上不算少見。
weak reference
Python內建有weakref module
,可以讓我們建立對某obj
的weak reference
。當該obj
的strong reference
為0
時,此weak reference
會收到通知,並且在有提供callback function
時,呼叫這個function
。而weakref.WeakKeyDictionary
是一個可以自動幫我們建立及移除weak reference
的方便容器。
想要能夠建立weak reference
,其必須有__weakref__
attribute
。__weakref__
是一個data descriptor
,當我們對一個obj
建立weak reference
時,這個weak reference object
其實就是存在__weakref__
中。
當使用slots
時,必須手動將__weakref__
加進__slots__
,否則將無法建立weak reference
。
class MyClass:
__slots__ = ('__weakref__',)
註1:至於想實作non-data descriptor
的朋友,相信可以由比較複雜的data descriptor
中融會貫通而來。
註2:可以參考Data Model對這部份的說明。